﻿using System;
using System.Globalization;

namespace CyberConnect.Insteon
{
    /// <summary>
    /// Encapsulates an Insteon device address.
    /// </summary>
    [System.ComponentModel.ImmutableObject(true)]
    public struct DeviceAddress : IEquatable<DeviceAddress>, IEquatable<int>
    {
        #region Fields

        /// <summary>
        /// Internal storage mechanism for device address 24-bit integer.
        /// </summary>
        private readonly int _DeviceId;

        /// <summary>
        /// An empty device address.
        /// </summary>
        public static readonly DeviceAddress Empty = new DeviceAddress();

        #endregion Fields

        #region Properties

        /// <summary>
        /// High value byte in the device address.
        /// </summary>
        public byte High { get { return (byte)(_DeviceId >> 16); } }

        /// <summary>
        /// Middle value byte in the device address.
        /// </summary>
        public byte Middle { get { return (byte)(_DeviceId >> 8 & 0xFF); } }

        /// <summary>
        /// Low value byte in the device address.
        /// </summary>
        public byte Low { get { return (byte)(_DeviceId & 0xFF); } }

        #endregion Properties

        #region Constructors

        /// <summary>
        /// Creates leftValue DeviceMessage using leftValue 24-bit integer.
        /// </summary>
        /// <param name="deviceId">Integer that represents a DeviceAddress.</param>
        public DeviceAddress(int deviceId)
        {
            if (deviceId > 0xFFFFFF)
                throw new ArgumentOutOfRangeException("deviceId", deviceId, "Out of 24-bit device address range.");

            this._DeviceId = deviceId;
        }

        /// <summary>
        /// Uses 3 bytes High, Middle, and Low respectively to define an address.
        /// </summary>
        /// <param name="deviceId">Byte array that holds the address.</param>
        /// <param name="offset">Start index of the address portion of the array.</param>
        public DeviceAddress(byte[] deviceId, int offset)
        {
            if (deviceId == null)
                throw new ArgumentNullException("deviceId");
            checked
            {
                if (deviceId.Length < 3 + offset)
                    throw new ArgumentOutOfRangeException("deviceId", "Not enough bytes provided!");

                this._DeviceId = deviceId[0 + offset] << 16 | deviceId[1 + offset] << 8 | deviceId[2 + offset];
            }
        }

        /// <summary>
        /// Uses 3 bytes High, Middle, and Low respectively to define an address.
        /// </summary>
        /// <param name="deviceId">Byte array that holds the address.</param>
        public DeviceAddress(byte[] deviceId)
            : this(deviceId, 0) { }

        /// <summary>
        /// Uses 3 bytes High, Middle, and Low respectively to define an address.
        /// </summary>
        /// <param name="high">First byte of the address.</param>
        /// <param name="middle">Second byte of the address.</param>
        /// <param name="low">Third byte of the address.</param>
        public DeviceAddress(byte high, byte middle, byte low)
        {
            this._DeviceId = high << 16 | middle << 8 | low;
        }

        /// <summary>
        /// Separates and converts a string into appropriate address bytes.
        /// </summary>
        /// <param name="deviceId">String containing address delimited by a character.</param>
        /// <param name="separator">Character used to delimit address bytes.</param>
        /// <example>
        /// FF.00.FF or FF-FF-00
        /// </example>
        public DeviceAddress(string deviceId, char separator)
            : this(StringToBytes(deviceId, separator)) { }

        /// <summary>
        /// Separates and converts a string into appropriate address bytes.
        /// </summary>
        /// <param name="deviceId">String containing address delimited by a period.</param>
        /// <example>
        /// FF.00.FF
        /// </example>
        public DeviceAddress(string deviceId)
            : this(StringToBytes(deviceId, '.')) { }

        #endregion Constructors

        #region Equality & Conversion Methods

        /// <summary>
        /// Separates and converts a string into appropriate address bytes.
        /// </summary>
        /// <param name="deviceId">String containing address delimited by a period.</param>
        /// <example>
        /// FF.00.FF
        /// </example>
        public static byte[] StringToBytes(string deviceId)
        {
            return DeviceAddress.StringToBytes(deviceId, '.');
        }

        /// <summary>
        /// Separates and converts a string into appropriate address bytes.
        /// </summary>
        /// <param name="deviceId">String containing address delimited by a character.</param>
        /// <param name="separator">Character used to delimit address bytes.</param>
        /// <example>
        /// FF.00.FF or FF-FF-00
        /// </example>
        public static byte[] StringToBytes(string deviceId, char separator)
        {
            if (string.IsNullOrWhiteSpace(deviceId) || deviceId.Length != 8)
            {
                if (deviceId == null)
                    throw new ArgumentNullException("deviceId");
                else
                    throw new FormatException("No DeviceId provided or incorrect length.");
            }

            byte[] AddressBytes = new byte[3];

            try
            {
                string[] AddressChunks = deviceId.Split(separator);

                for (int i = 0; i < 3; i++)
                {
                    AddressBytes[i] = byte.Parse(AddressChunks[i], NumberStyles.AllowHexSpecifier);
                }
            }
            catch (FormatException ex)
            {
                throw new FormatException("An address chunk was in an incorrect format", ex);
            }

            return AddressBytes;
        }

        /// <summary>
        /// Gets device address bytes in an array.
        /// </summary>
        /// <returns>Device address in bytes High, Middle, Low respectively.</returns>
        public byte[] ToBytes()
        {
            return new byte[3] { High, Middle, Low };
        }

        /// <summary>
        /// Gets device address in dotted byte format.
        /// </summary>
        /// <returns>String representing the device address in dotted byte format.</returns>
        public override string ToString()
        {
            return string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}",
                High.ToString("X2", CultureInfo.InvariantCulture),
                Middle.ToString("X2", CultureInfo.InvariantCulture),
                Low.ToString("X2", CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Returns a value indicating whether this instance is equal to a specified object.
        /// </summary>
        /// <param name="obj">An object to compare with this instance. </param>
        /// <returns>if obj is an instance of DeviceAddress and equals the value of this instance; otherwise, false.</returns>
        public override bool Equals(object obj)
        {
            return obj == null ? false
                : !(obj is DeviceAddress) ? false
                    : Equals((DeviceAddress)obj);
        }

        /// <summary>
        /// Returns the hash code for this instance.
        /// </summary>
        /// <returns>A 32-bit signed integer hash code.</returns>
        public override int GetHashCode()
        {
            return this._DeviceId.GetHashCode();
        }

        /// <summary>
        /// Returns a value indicating whether this instance is equal to a specified DeviceAddress value.
        /// </summary>
        /// <param name="other">A DeviceAddress value to compare to this instance.</param>
        /// <returns>true if obj has the same value as this instance; otherwise, false.</returns>
        public bool Equals(DeviceAddress other)
        {
            return this._DeviceId == other._DeviceId;
        }

        /// <summary>
        /// Returns a value indicating whether this instance is equal to a specified Int32 value.
        /// </summary>
        /// <param name="other">An Int32 value to compare to this instance.</param>
        /// <returns>true if obj has the same value as this instance; otherwise, false.</returns>
        public bool Equals(int other)
        {
            return this._DeviceId == other;
        }

        /// <summary>
        /// Returns a value indicating whether two DeviceAddresses are equal.
        /// </summary>
        /// <seealso cref="operator !=(DeviceAddress, DeviceAddress)"/>
        /// <returns>true if the values are equal; otherwise, false.</returns>
        public static bool operator ==(DeviceAddress leftValue, DeviceAddress rightValue)
        {
            return leftValue.Equals(rightValue);
        }

        /// <summary>
        /// Returns a value indicating whether a DeviceAddresses and an Int32 are equal.
        /// </summary>
        /// <seealso cref="operator !=(DeviceAddress, int)"/>
        /// <returns>true if the values are equal; otherwise, false.</returns>
        public static bool operator ==(DeviceAddress leftValue, int rightValue)
        {
            return leftValue.Equals(rightValue);
        }

        /// <summary>
        /// Returns a value indicating whether two DeviceAddresses are NOT equal.
        /// </summary>
        /// <seealso cref="operator ==(DeviceAddress, DeviceAddress)"/>
        /// <returns>true if the values are NOT equal; otherwise, false.</returns>
        public static bool operator !=(DeviceAddress leftValue, DeviceAddress rightValue)
        {
            return !(leftValue == rightValue);
        }

        /// <summary>
        /// Returns a value indicating whether a DeviceAddresses and an Int32 are NOT equal.
        /// </summary>
        /// <seealso cref="operator ==(DeviceAddress, int)"/>
        /// <returns>true if the values are NOT equal; otherwise, false.</returns>
        public static bool operator !=(DeviceAddress leftValue, int rightValue)
        {
            return !(leftValue == rightValue);
        }

        /// <summary>
        /// Allows casting an Int32 to DeviceAddress.
        /// </summary>
        /// <param name="address">Int32 to convert to a DeviceAddress</param>
        /// <returns>DeviceAddress matching the Int32 provided.</returns>
        public static explicit operator DeviceAddress(int address)
        {
            return new DeviceAddress(address);
        }

        #endregion Equality & Conversion Methods
    }
}